



 |
Charlie Calvert's C++ Builder Unleashed
- 28 -
Game Programming
Overview
In this chapter and the next, you'll examine multimedia and game objects. In particular,
you will see the following:
- A simple two-dimensional graphics engine that uses DirectDraw
- A simple game engine
- Examples of how to use both objects
- Various comments on object design
- The opening portions of a strategy game called Byzantium
The graphics engine used in this chapter is implemented on disk both in C++ and
in Object Pascal, and it appears to BCB programmers as a set of simple components.
The game engine, shown in the next chapter, is implemented in C++, and it appears
to the user as a set of objects. In short, the graphics engine consists of components,
and the game engine consists of simple objects.
You can use all the code found in this book in your own programs, but you cannot
use the art or designs found in this chapter and the next. My objection is to distributing
the art in your own programs. You are free to use the art in your own home or at
your company, but you cannot distribute it to others in any form. You can use the
game engine to build your own games, but it must be distinct from the game you find
in the next chapter. The world must be different, and so should the art.
I hope that many readers are specifically interested in developing and using game
engines. If you're not interested in that subject, I still think that you will find
these chapters among the most interesting in the book. In particular, in these pages
you will see how to create complex sets of objects and use them in real-world programs
that have to perform complex tasks.
The DirectX technology shown in these chapters can be used in a wide variety of
applications. For example, you can use the graphics engine shown here to create the
following:
- Games
- Simulations
- Presentations
- Advanced graphical features of all kinds
Besides describing the raw graphics tools in this chapter, I also show you how
to create complex object hierarchies, how to plan and design the functionality of
an object properly, how to divide groups of objects into specific engines, and how
to get the engines to work together.
The overriding theme of these chapters is the importance of object design. You
have to learn how to create objects that are reusable and how to design object hierarchies
that are simple enough to meet the needs of real programmers.
In the end, everything comes down to design. If you design the right kinds of
objects, you can accomplish almost anything. Furthermore, as you will see, the judicious
use of objects can help you dig yourself out of almost any hole. Objects are the
Holy Grail of contemporary programming, and if you understand how to use them correctly,
they will help you complete project after project on time.
Just understanding the basics of object technology is not enough. Just knowing
about polymorphism, inheritance, and encapsulation is not enough. You also need to
know how to design objects that you can use and that other people can use. This section
of the book is about designing these kinds of objects. Of course, on the way, you
will also have the chance to engage in a good deal of fun with graphics, games, and
multimedia.
Game Resources
Game programming is more complicated than most standard Windows programming tasks
for many reasons. The root of this complexity is the need to use special graphics
tools such as DirectX. Another complication that many first-time game programmers
underestimate involves creating art.
Many books are available on game programming and on producing art for games. Two
that I have found useful are
Tricks of the Game Programming GurusI?, LaMothe, Ratcliff,et al., Sams Publishing
The Ultimate Game Developers Sourcebook, Ben Sawyer, Coriolis Group Books.
You will also need a paint program. I find that the inexpensive shareware program
called Paint Shop Pro meets most of my needs, though many other powerful programs
such as Fractal Paint (www.fractal.com) are
available. Here's the contact information for Paint Shop Pro:
JASC, Inc.
P.O. Box 44997
Eden Prairie, MN 55344
930-9171
www.jasc.com
Other key paint programs that I use often include TrueSpace (www.caligari.com)
and VistaPro from Virtual Reality Laboratories in San Luis Obispo. TrueSpace allows
you to create three-dimensional objects, and VistaPro allows you to create landscapes.
The background scenes that show rolling hills, mountains, and trees in the figures
for this chapter and the next were created in VistaPro (Virtual Reality Laboratories
in San Luis Obispo, CA, 805-545-8515, e-mail: VRLI@aol.com,
WWW: http://www.romt.com). (See
the files called Backgrd1.bmp and Backgrd2.bmp on the CD that comes
with this book for examples of VistaPro files that have gone through a color reduction
so that they could fit on a 256-color palette.)
You might find additional game components, links, or code available at these sites:
http://www.silvercrk.com/directx/
http://www.spinlogic.com/GameDev/
http://users.aol.com/charliecal
Many other Web sites are of interest to game developers, but you can find links
to most of them from the sites listed here. On CompuServe, type GO GAMEDEV
to find the game developers' forum.
As I stated previously, game programming is an extremely complex undertaking.
The biggest mistake you can make is to try to create your own tools from scratch.
Use the tools included in this book to get some sense of what you can gain from a
graphics engine and a set of gaming tools. Then go out and search the Web and your
local bookstores for ready-made graphics and game engines. Build your games using
these tools; don't try to create your own tools from scratch unless you're sure,
double-sure, and then triple-sure you know what you're doing and why.
If you have never built a game before, then don't even consider building one from
scratch. Build your first game with someone else's engine. Then, after you understand
something about the tools that are available, you might finally be in a position
to consider creating some of your own tools. Even then, however, I would still recommend
turning around and using someone else's tools rather than trying to create your own.
The Ultimate Game Developers Sourcebook, mentioned previously, offers a great deal
of information on finding third-party game engines.
Where We're Headed: Object Overview
In this part of the book, you'll create two sets of objects. Using the first set
of objects, you can encapsulate DirectX multimedia functionality inside easy-to-use
components. The second set of objects allows you to create games, simulations, or
multimedia tools using the game engine.
The classic mistake to make in this process is to find or create the objects that
give you access to the multimedia and DirectX interfaces and to then think you're
ready to create a program. This error is fatal--and very common.
Suppose that you have a set of objects that give you full access to DirectDraw.
The mistake many programmers make is to assume that they can directly use these objects
when creating a game or simulation. Indeed, in some cases, that may be possible.
However, most of the time you need to create a second set of objects tailored specifically
for games or simulations. In particular, you need a TGameEngine object.
Furthermore, this object is not necessarily a direct descendant of any of your graphics-based
objects. It may use them via aggregation but probably does not descend directly from
them. The same applies for simulations: You need a TSimulation object that
uses your graphics objects but presents a different interface to the user. A game
object, for example, needs items such as TPlayer, TBoard, THero,
TScene, and so on. Your graphic objects don't know anything about those
kinds of objects.
To get a handle on this issue, you need to think in terms of specific problem
domains. One problem domain involves using DirectDraw to create fast graphics. A
second problem domain involves creating a game engine, and a third problem domain
involves creating a specific game. You need to draw firm lines between these domains
so that the complexity of one problem domain does not creep into another domain.
As a rule, you should not allow any direct calls to your graphics objects by the
users of your TGameEngine object. TGameEngine calls the graphics
objects, but it does so via private interfaces that TGameEngine users can
never see.
-
NOTE: An example might be helpful here. In DirectX,
you can write text to the screen using device contexts. You get the DC for a device,
copy objects into it, and then use TextOut to write to the screen. Having
the game engine use these same objects is probably not wise. Instead, you should
hide the underlying technology and provide a routine with a name such as WriteXY
or WriteLn or printf or Say that will let you write to
the screen. Internally, your implementation of WriteXY will use device contexts,
but that fact should be hidden from the user. This will allow you to switch your
game from Windows to DOS to the Mac without having to change its code. Only the interface
to the graphics engine changes, but your game code remains untouched.
One way to help design these objects correctly is to ensure that a user of TGameEngine
would have to do nothing more than recompile if you were to switch your game engine
technology from one set of graphics objects to another. In other words, the users
of TGameEngine should not have to know anything about the tools you use
to create fast graphics. You can tell them, "Hey, I use DirectDraw," or
"I use WinG," but the actual technology involved should not matter to them
in the sense that their code will still compile if you switch the background engine.
Users of TGameEngine should not have to know whether TGameEngine
uses DirectDraw or WinG. Users of TGameEngine never call the graphics objects
directly, and they don't even have to conceive of the fact that such a thing as an
IDirectDraw object exists.
Obviously, one of the important benefits of this system is that you can change
from WinG to DirectDraw without having to rewrite your game or game engine. But this
capability is not the primary benefit that interests me. What I like about this system
is that it creates domains for problems. If I'm working with the TGameEngine
object and find that it's not drawing things correctly because of some error in the
way I access DirectDraw, then I can put the TGameEngine object away, open
the graphics engine, and find out what is wrong. My game engine and my game cannot
be the cause of the problem because they don't talk to the IDirectDraw interface.
They don't know anything about the IDirectDraw interface!
Another benefit of this system is that it allows you to improve one part of your
code without affecting existing parts of your code. For example, I could drastically
rewrite the graphics engine without having to change the way TGameEngine
operates. In these situations, rewriting the graphics engine without changing its
interface is best, but even if you do change it, you can still buffer your game from
these changes because it talks to TGameEngine, not to the graphics engine.
However, the best thing to do is to never change a public interface after you create
it.
-
NOTE: I will concede that some of the theory
I am describing here is a bit idealized. In practice, I rarely achieve precisely
the degree of object independence described in the preceding paragraphs. However,
the closer I can get to it, the better off I am. Aiming as high as possible is always
best, thereby limiting as much as possible the amount of trouble you will have if
your architecture needs to be upgraded.
Furthermore, the process of learning to design good objects is incremental at best.
The first time I tried to create a complex set of objects, I failed miserably. The
second time, I took to heart some of the ideas I am describing here and came closer
to achieving my goal. The third time out I made yet fewer gross design errors, and
so on, till finally I was starting to produce complex object hierarchies that could
actually be used and maintained.
Creating Public Interfaces
The last paragraph of the preceding section introduces a second key rule of object-oriented
design. Creating problem domains is the most important step, but a second crucial
condition is creating public interfaces that have little to do with your underlying
technology.
When you're creating the public interface for the graphics engine, you should
take off your IDirectDraw expert hat and put on one that helps you pretend
you are the user of this object. What will a user of this object want to see? A user
doesn't want to hear about the nitty-gritty of using IDirectDraw; instead,
he or she wants to be able to work with broad concepts such as TBackground,
TSprite, and TWindow.
Managing Complexity: Create Humble Objects
Before going further, I want to talk just a little more about limiting the problem
domain.
One of the most important jobs that objects can do for you is to help manage complexity.
In the first chapter of this book, I stressed the importance of being humble when
you program. Here, that theme can really start to pay off.
The worst mistake you can make as a programmer is to decide "Hey, I'm so
smart, so cool, that I can make this object stunningly complex and efficient and
still be able to use it." In the process, you might end up continually collapsing
one object into another and continually finding fast, clever ways to get tasks done
in the fewest possible moves. All this cleverness is good and well, if you can simultaneously
produce objects that you and others can use and maintain.
Putting all the logic into one set of objects has the following problems:
- Any hope of code reuse is obliterated. If one small set of objects forms the
whole game, how can you separate the reusable part of the code for a second game
that has similar characteristics?
- Maintaining it is a nightmare. If something goes wrong anywhere in the game,
you have no way of localizing a problem and testing it separately from the rest of
the application. If all you have are a few big, complex objects, when something goes
wrong, all the lines of code in your program are suspect, and you cannot separate
out the code you need to test and repair. This monumental problem is likely to sink
most projects completely. You have to be able to break down a program into its various
components and test each part separately.
- Teaching someone how to work with the code for a game is extremely hard if he
or she has to understand the entire game to be able to work on any part of it. Understanding
one huge, complex object is difficult. The paradigm is innately complex. You have
a much simpler task getting someone up to speed on some code if you can say, "Here,
we need to make these changes to this 500-line object. Please get to work right away;
time is of the essence." The nightmare scenario sounds like this: "Welcome
aboard. We have 200,000 lines of code here, and we need to make some changes to it.
Please spend the next six months becoming familiar with our code base, and then I
would like you to make some changes to a few small portions of it. Tell me when you're
up to speed."
Putting all the logic in clearly delineated sets of objects has the following
advantages, which are the converse of the disadvantages laid out in the preceding
three points.
- Code reuse is supported. A well-designed graphics engine can be reused in almost
any graphics-based game. A subset of those objects can be used in virtually any multimedia
venture that requires DirectDraw. The same is true with the GameEngine code.
It's all in one place and can be reused. Conversely, the code that is specific to
Byzantium is all in one place.
- Maintaining it is easy. If something goes wrong in the code, you can isolate
the problem in a few distinct objects that you can test separately. Problems in one
part of the code don't automatically percolate into other, related domains. Instead,
they are isolated in one section of the code. This problem is like a leak in a submarine:
If one section of the ship is damaged, you can seal it off from the rest of the ship.
It won't affect the whole program; it is isolated in one area. You can then concentrate
on fixing that area without worrying about the ramifications of those repairs on
the rest of the program.
- Understanding it is easy. You know exactly what is going on in the graphics engine
components because the problem domain is manageable. You can explain the basic structure
of the program to someone in five minutes because each part plays a distinct, easy-to-understand
role.
The theme underlying all these points is humility. To be a good programmer, you
have to admit that a certain level of complexity simply overwhelms you. Your only
bulwark against that confusion is to break down the project into manageable chunks
that you can understand.
Humble programmers produce real programs. Hotshots are going to produce fantastic
programs Real Soon Now. I know that's true because those hotshots have been telling
me about their program ideas for years.
The DirectX Game Engine
In the next few sections of the chapter, I describe a set of components that make
up a simple graphics engine. The key components in this graphics engine are described
in Table 28.1.
Table 28.1. The key objects in the graphics engine.
| Component |
Purpose |
| THermes |
Get into and out of DirectX Exclusive mode |
| TScene |
Draw the background of a graphics scene |
| TSpriteScene |
Draw the background of a graphics scene that supports sprites |
| THermesChart |
Create a tiled game scene |
After I describe the graphics engine, I will describe the game engine that can be
used in conjunction with these objects.
THermes: Getting Into and Out of Exclusive Mode
DirectX is a Microsoft technology that allows you to write high-performance gaming,
simulation, and multimedia programs. In this chapter, I focus on the IDirectDraw
interface, which is a portion of DirectX that gives you the ability to write directly
to the video buffer.
The first thing you find when working with DirectX is that, to take full advantage
of it, you need to switch into a special video mode called Exclusive mode. For the
purposes of this book, this mode has a 640x480 screen dimension and 256 colors. Actually,
other screen sizes and color resolutions are available to you as a DirectX programmer,
but THermes gives you access only to this one screen mode.
The first object in the graphics engine is designed to switch you into and out
of this mode. The class declaration for this object is shown in Listing 28.1. You
can find the full source for the code shown here in a file called Mercury2.cpp
in the Utils directory on the CD that accompanies this book.
The Mercury unit relies on Creatures.pas, which ships with this
book, and DDraw.h, which ships in the included directory with BCB. You will
also find a Pascal version of the Mercury unit in the Units subdirectory
on the CD that accompanies this book.
-
NOTE: To find some Web sites of interest to DirectX
programmers and full Pascal translations of DirectX 3.X headers, go to the following:
http://www.dkw.com/bstone/
When you're looking at this code, keep these points in mind:
- I do not quote the entire source until the end of the chapter. Throughout the
main body of this chapter, I quote only the class declarations, one at a time as
needed so that you can use them to grasp the structure of the portion of code currently
under discussion. At the end of the chapter, I quote the entire object.
- You can install all the key objects in these units as components and use them
directly in BCB, just as you would use any of the other components discussed in this
book.
- You don't really have to understand this code, but you do need to know how to
use the components created when these objects are installed on the Component Palette.
Listing 28.1. Mercury2.cpp contains logic for handling
sprites, backgrounds, and DirectX. The key object in this file is THermes.
class THermes : public Classes::TComponent
{
typedef Classes::TComponent inherited;
friend THermesChart;
friend TDraw;
private:
bool FActive;
Creatures1::TFileCreatureList* FCreatureList;
HWND FHandle;
bool FTimerOdd;
int FTimerInterval;
bool FExclusive;
Classes::TNotifyEvent FPaintProc;
TScene* FScene;
bool FUseTimer;
bool FFirstTime;
IDirectDraw* FDirectDraw;
IDirectDrawSurface* FBackSurface;
IDirectDrawClipper* FClipper;
IDirectDrawSurface* FPrimarySurface;
bool __fastcall CreatePrimary(void);
void __fastcall DDTest(long hr, System::AnsiString S);
void __fastcall InitBaseObjects(void);
bool __fastcall MakeItSo(long DDResult);
void __fastcall SetScene(TScene* Scene);
bool __fastcall SetUpBack(void);
protected:
void __fastcall DrawBitmaps(void);
virtual long __fastcall RestoreSurfaces(void);
public:
__fastcall virtual THermes(Classes::TComponent* AOwner);
__fastcall virtual ~THermes(void) {}
void __fastcall EndExclusive(void);
void __fastcall ErrorEvent( System::AnsiString S);
void __fastcall Flip(void);
virtual void __stdcall InitObjects(void);
virtual void __fastcall Run(void);
__property bool Active = {read=FActive, write=FActive, nodefault};
__property IDirectDrawSurface* BackSurface =
{read=FBackSurface, write=FBackSurface, nodefault};
__property Classes::TNotifyEvent OnDrawBitmap =
{read=FPaintProc, write=FPaintProc};
__published:
__property Creatures1::TFileCreatureList* CreatureList =
{read=FCreatureList, write=FCreatureList, nodefault};
__property bool Exclusive = {read=FExclusive, write=FExclusive, nodefault};
__property TScene* Scene = {read=FScene, write=SetScene, nodefault};
__property int TimerInterval =
{read=FTimerInterval, write=FTimerInterval, nodefault};
__property bool UseTimer = {read=FUseTimer, write=FUseTimer, nodefault};
};
To install THermes and the other graphics engine objects, choose Component
| Install. Click the Add button, and then browse the Units directory from
the CD that accompanies this book. Install both Creatures1.pas and Mercury2.cpp.
To use THermes, start a new project, drop a THermes object on
it, and create an OnKeyDown handler that closes the form if any key is pressed.
The code for such a project is shown in Listings 28.2 and 28.3.
Listing 28.2. The HermesTest1 project shows how to use THermes. The header for the
main unit is shown here.
///////////////////////////////////////
// Main.h
// Testing the THermes object
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include "..\..\utils\Mercury2.h"
#include <vcl\Menus.hpp>
class TForm1 : public TForm
{
__published:
THermes *Hermes1;
TMainMenu *MainMenu1;
TMenuItem *Run1;
void __fastcall FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift);
void __fastcall Run1Click(TObject *Sender);
private:
void __fastcall MyExceptions(TObject *Sender, Exception *E);
public:
__fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 28.3. The source for the main unit in the HermesTest1
project.
///////////////////////////////////////
// Main.cpp
// The TestHermes project tests the THermes component
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma link "Mercury2"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
Application->OnException = MyExceptions;
}
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key,
TShiftState Shift)
{
if ((Shift.Contains(ssAlt)) && (Key == `E'))
throw Exception("Some Exception");
else
if (!Shift.Contains(ssAlt))
Close();
}
void __fastcall TForm1::Run1Click(TObject *Sender)
{
Hermes1->Run();
}
void __fastcall TForm1::MyExceptions(TObject *Sender,
Exception *E)
{
Hermes1->EndExclusive();
ShowMessage(E->Message);
}
You should note that this project probably will not run correctly unless you add
Creatures.pas to it, as shown in the USEUNIT statement from the
project source:
//--------------------------------------------------------------------------
#include <vcl\vcl.h>
#pragma hdrstop
//--------------------------------------------------------------------------
USEFORM("Main.cpp", Form1);
USERES("HermesTest1.res");
USEUNIT("..\..\Units\creatures1.pas");
//--------------------------------------------------------------------------
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TForm1), &Form1);
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}
//--------------------------------------------------------------------------
Run the project once with Exclusive set to False. Click the
Run button once to switch on DirectX. In this mode, you should see the color of the
main form change, but nothing else very special will happen.
Go back into design mode, and check once to make sure that you have not placed
any components on the main form other than the menu and the THermes. The
rest of the form must be free of visual controls so that you can detect keystrokes
directly on the surface of the form.
Set Exclusive to True and run the program again. Click the Run
menu item, and the whole screen should go blank as you switch into Exclusive
mode. To get out of the mode, simply press any key.
If you want, you can change the code for the OnKeyDown event:
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key,
TShiftState Shift)
{
if ((Shift.Contains(ssAlt)) && (Key == `X'))
Close();
}
With this code in place, you will be able to press Alt+Tab to move away from the
main window of the program and switch out of Exclusive mode. You can then
press Alt+Tab to move back to the program and press Alt+X to exit. When you're pressing
these keys, the actual picture drawn on your screen when in Exclusive mode
is undefined because THermes does not control the output in Exclusive
mode; it controls only the act of entering and exiting Exclusive mode.
DirectX allows you to run applications in a windowed mode. That is, you don't
have to slip into Exclusive mode to run DirectX applications. However, windowed
mode is not very good in terms of performance. In fact, you'll find little advantage
to a windowed mode DirectX application over a GDI-based standard Windows program.
I give you access to windowed mode primarily because it is valuable when you're
debugging an application. In short, you should run your DirectX applications in windowed
mode while you're debugging them so that you can step through the debugger while
viewing your application's output. After you set up your application correctly, switch
into Exclusive mode and test it.
If an exception happens when you're in Exclusive mode, that is a bad
thing. In particular, your program will appear to lock up, and you may be forced
to reboot. Following the steps outlined in the preceding paragraphs helps you eliminate
the possibility that this will happen. However, you cannot be sure that an exception
will not be raised in your application. As a result, you might want to override the
exception handler for your application in this case:
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
Application->OnException = MyExceptions;
}
void __fastcall TForm1::MyExceptions(TObject *Sender,
Exception *E)
{
Hermes1->EndExclusive();
ShowMessage(E->Message);
}
The constructor for the main form designates a custom routine called MyExceptions
to handle all the exceptions that occur in this application. Whenever an unhandled
exception occurs, it will be passed to this routine. When I get the exception, I
call Hermes1->EndExclusive, which pops the application out of Exclusive
mode, therefore making it possible to open the dialog box that shows the error message.
-
NOTE: This system will not do you any good, of
course, unless you turn off the Break On Exception option, which you can find by
choosing Options | Environment | Preferences. If you are in Exclusive mode, and the
IDE tries to take you to the line of code in your application where an exception
occurred, your system will appear to lock up.
As a rule, I will not show you the underlying Object Pascal code that makes this
component work. However, in this one case, I will show you what goes on:
procedure THermes.EndExclusive;
begin
if (FDirectDraw <> nil) then begin
FDirectDraw.FlipToGDISurface;
if FExclusive then
FDirectDraw.SetCooperativeLevel(FHandle, DDSCL_Normal);
end;
end;
This code first flips away from DirectX to GDI mode and then cuts out of Exclusive
mode by setting the cooperative level to normal. When I take the application into
Exclusive mode, I make the same call but with different parameters:
DDTest(DirectDrawCreate(nil, FDirectDraw, nil), `InitObjects1');
if not FExclusive then
Flags := DDSCL_NORMAL
else
Flags := DDSCL_EXCLUSIVE or DDSCL_FULLSCREEN;
DDTest(FDirectDraw.SetCooperativeLevel(FHandle, Flags),'SetCooperativeLevel');
if FExclusive then begin
hr := FDirectDraw.SetDisplayMode(640, 480, 8);
if(hr <> DD_OK) then
raise EDDError.CreateFmt(`THermes.InitObjects: %d %s',
[hr, GetOleError(hr)]);
end;
Notice that my EndExclusive method makes no attempt to set the display
mode back to the dimensions and bit depth selected by the user in his or her system
configuration. Instead, I just leave it to the user to close the application, which
will return the screen to its normal resolution. My goal in using EndExclusive
is just to keep the system from hanging.
TScene: Drawing a Background
After you know how to get into and out of Exclusive mode, the next step
is to learn how to draw a picture to the screen. Most games consist of many pictures,
but I start out showing you how to use a simple object that just displays a single
bitmap. For most people, this object is not really very useful, in that there is
no point in using DirectX if you want to show only one picture. However, TScene
serves as a base class for other objects that handle more complex tasks. I will therefore
spend a few moments showing you how to use it so that you will understand the class
on which the other objects are built.
You can find the declaration for the class and some code that uses it in Listings
28.4 through 28.6. Again, I show only the class declarations for the Pascal code
and give the complete listings for the C++ code.
Listing 28.4. The class declaration for the key parts of
the TScene and TDraw objects.
class TDraw;
class TDraw : public Classes::TComponent
{
typedef Classes::TComponent inherited;
friend THermesTiler;
friend THermesChart;
friend TScene;
friend TSpriteScene;
private:
AnsiString FDLLName;
THermes* FHermes;
HANDLE FLib;
IDirectDrawPalette* FPalette;
int FTransparentColor;
bool __fastcall CreateDDSurface(IDirectDrawSurface* &DDS,
System::AnsiString BitmapName, bool UsePalette);
IDirectDrawSurface* __fastcall CreateSurface(HANDLE Bitmap);
HANDLE __fastcall GetDib(HANDLE Instance, System::AnsiString S);
IDirectDrawPalette* __fastcall LoadPalette(HANDLE Instance,
const System::AnsiString BitmapName);
public:
__fastcall virtual TDraw(Classes::TComponent* AOwner);
__fastcall virtual ~TDraw(void);
void __fastcall WriteXY(int X, int Y, System::AnsiString S);
__property int TransparentColor =
{read=FTransparentColor, write=FTransparentColor, nodefault};
__published:
__property AnsiString DLLName =
{read=FDLLName, write=FDLLName};
};
class TScene : public TDraw
{
typedef TDraw inherited;
friend THermes;
private:
System::AnsiString FBackgroundBitmap;
Graphics::TColor FBackColor;
RECT FBackRect;
tagPOINT FBackOrigin;
bool FBlankScene;
bool FShowBitmap;
IDirectDrawSurface* FWorkSurface;
Classes::TNotifyEvent FOnSetupSurfaces;
Classes::TNotifyEvent FOnDrawScene;
public:
__fastcall virtual TScene(Classes::TComponent* AOwner);
virtual void __fastcall DestroyObjects(void);
virtual void __fastcall DrawScene(void);
virtual long __fastcall RestoreSurfaces(void);
virtual void __fastcall SetupSurfaces(System::TObject* Sender);
__published:
__property System::AnsiString BackgroundBitmap =
{read=FBackgroundBitmap, write=FBackgroundBitmap, nodefault};
__property bool BlankScene = {read=FBlankScene, write=FBlankScene, nodefault};
__property long OriginX ={read=FBackOrigin.x, write=FBackOrigin.x, nodefault};
__property long OriginY ={read=FBackOrigin.y, write=FBackOrigin.y, nodefault};
__property bool ShowBitmap = {read=FShowBitmap, write=FShowBitmap, default=1};
__property Classes::TNotifyEvent OnDrawScene =
{read=FOnDrawScene, write=FOnDrawScene};
__property Classes::TNotifyEvent OnSetupSurfaces =
{read=FOnSetupSurfaces, write=FOnSetupSurfaces};
__property Graphics::TColor BackColor =
{read=FBackColor, write=FBackColor, nodefault};
__property TransparentColor ;
public:
__fastcall virtual ~TScene(void) { }
};
Listing 28.5. The header for the unit from the SceneTest1
project that tests the TScene object.
///////////////////////////////////////
// Main.h
// Test the TScene object
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\Menus.hpp>
#include "Mercury2.h"
class TForm1 : public TForm
{
__published:
TScene *Scene1;
THermes *Hermes1;
TMainMenu *MainMenu1;
TMenuItem *Run1;
void __fastcall Run1Click(TObject *Sender);
void __fastcall FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift);
private:
public:
__fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 28.6. The main form for the unit that tests the
TScene object.
///////////////////////////////////////
// Main.cpp
// Test the TScene object
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma link "Mercury2"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::Run1Click(TObject *Sender)
{
Hermes1->Run();
}
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key,
TShiftState Shift)
{
if ((Shift.Contains(ssAlt)) && (Key == `X'))
Close();
}
The program shown here can switch you into Exclusive mode and show a picture
like the one shown in Figure 28.1.
Figure 28.1. The main screen of
the TestScenel program in windowed mode.
To get started with the TScene object, drop both it and the THermes
object on a form. Use the Scene property of THermes to connect
it to the TScene object. Click the TScene object, and select a
background bitmap by browsing the Chap28/Media directory included on the
CD that accompanies this book. Select any bitmap that is smaller than or equal to
640x480 and that has 256 colors in it. For example, you might choose BackGrd2.bmp.
Set the Transparent color to black, which will appear at offset zero on the bitmaps.
Set the BackColor property of the bitmap to the same shade, and set BlankScene
to True:
TransparentColor = 0;
BackColor = clBlack;
BlankScene = True;
-
NOTE: TransparentColor refers
to the color in a bitmap that will be transparent. That is, the areas in the bitmap
where this color is present become transparent. You need a transparent color when
you want to blit an irregularly shaped bitmap on top of a background bitmap. For
example, if you have a picture of a bird that you want to blit onto a picture of
the sky, just set the background area in the picture of the bird to the transparent
color, and it will not show up when the bird is blitted to the screen. In other words,
the picture of the sky will show through the transparent area, so the bird will appear
directly against the sky, with no intervening border.
You choose the selected transparent color from an offset in the palette of the bitmap
you're using. For example, if you set TransparentColor to 0, the
first color in the bitmap's palette will be the transparent color. Most good paint
programs will show you the palette for a picture. For example, Paint Shop Pro provides
this service.
If you select a prominently used color from a picture as the transparent color,
the portions of the picture with that color in them will look strange at runtime.
To avoid this situation, set the BackColor property of TScene and
the TransparentColor for the picture to the same shade. Also set BlankScene
to True. This way, the transparent color will look through onto a background
that is the same shade as the color you're making transparent.
The BlankScene property blanks the background of the TScene
object to the color found in the BackColor property. This capability can
be useful in the current situation and also when you have a small bitmap that you
want to show against a blank window set to some particular color.
When you're creating multimedia applications in Windows, the palette of your bitmaps
is very important. In particular, you should usually create the same 256-color palette
for all your bitmaps. Use a program such as Paint Shop Pro to set up these bitmaps.
Most of the bitmaps included in the Chap28/Media directory on the CD that
comes with this book have a palette designed for pictures that have lots of green
fields, brown earth, and a range of skin tones.
When you're working with only one bitmap in a scene, all this business about palettes
seems unnecessarily complicated. However, after you start adding sprites to the scene,
you will be glad that you have some way to set up background colors and to set up
TransparentColor. I explain more about palettes in the next section of this
chapter.
The TScene object also has a built-in timer that will continually redraw
the picture at preset intervals. This capability is not particularly useful in the
case of a program that blits only one picture to the screen. However, when you start
working with multiple bitmaps, some of which are animated, this capability becomes
a key feature. In particular, DirectX allows you to draw to an offscreen buffer,
which is then blitted to the screen almost instantly. Using this method is an excellent
way to achieve smooth animation.
Instead of using a timer, you can also handle the TApplication::OnIdle
event, and then call Hermes1->Flip inside that event handler. The
OnIdle event will be called whenever the CPU is not otherwise occupied.
To get the TScene object set up correctly, you should turn on the timer
and set the TimerInterval property to some reasonable value, such as 250
milliseconds. Setting this property will cause the picture to be refreshed four times
a second. This number is too slow for animation, but it works fine for the simple
scene created here. In particular, if you press Alt+Tab to move away from the DirectX
program, the timer will automatically restore the picture when you press Alt+Tab
to move back to the main scene.
On the CD that accompanies this book, you will find a program called TestScene1.
The source for this program is shown in Listings 28.5 and 28.6. You can use this
code as a guide for using the TScene object. Remember that TScene
is not really meant to be used in a real program. It is just a base object from which
other, more useful objects can descend.
Art and Palettes
In Windows and DOS games, you almost always work with a 256-color palette. Someday
this will change, but for now you should get used to working with these palettes
and come to understand them well. An in-depth description of palettes is available
in some of the books and Web sites mentioned in the section called "Games Resources"
at the beginning of this chapter.
Using Paint Shop Pro, you can save a palette to disk as a text file:
JASC-PAL
0100
256
0 0 0
128 0 0
0 128 0
128 128 0
0 0 128
128 0 128
0 128 128
192 192 192
176 136 72
216 192 160
... // Many numbers omitted here...
240 224 208
160 160 164
128 128 128
255 0 0
0 255 0
255 255 0
0 0 255
255 0 255
0 255 255
255 255 255
Here is how to decode the first few lines of this file:
JASC-PAL
0100
256
0 0 0
JASC-PAL identifies the palette as belonging to Paint Shop Pro. The second
line defines the number of entries in the palette in hexadecimal format. The third
line has the number of entries in base ten notation. The fourth line is the first
member of the palette in RGB format, with all values set to 0, which is
black.
Notice that my palette runs through various shades:
118 102 237
117 116 240
129 113 249
155 110 239
107 144 140
115 144 140
123 136 140
123 148 140
111 148 148
Clearly, I'm interested in this area of the palette.
Obviously, you don't want to have to work with raw numbers like this. Paint Shop
Pro will let you view a palette as an array of colors, as shown in Figure 28.2. Having
some kind of tool that will help you design your palette is essential. You should
also find books, such as this one and the ones mentioned previously, that include
sample palettes.
Figure 28.2. Using Paint Shop Pro
to view an array of shades that make up a 256-color palette.
Working with TSpriteScene
Now that you know how to show a simple picture in DirectX mode, and now that you
know a bit about palettes, you're ready to start working with sprites. The sprite
support I have at this time is very rudimentary, as you can see in Listings 28.7
through 28.9. You could, however, use these base sprite classes to create sprites
that are more full-featured. You should also check my Web site for updates to this
code.
Listing 28.7. The declarations for the TSpriteScene and
TScene classes.
class TSprite : public Classes::TComponent
{
typedef Classes::TComponent inherited;
friend TSpriteScene;
private:
System::AnsiString FBitmap;
tagPOINT FPosition;
IDirectDrawSurface* FSurface;
RECT FRect;
public:
bool __fastcall IsHit(int X, int Y);
__property IDirectDrawSurface* Surface=
{read=FSurface, write=FSurface, nodefault};
__property RECT Rect = {read=FRect, write=FRect};
__published:
__property System::AnsiString Bitmap={read=FBitmap, write=FBitmap, nodefault};
__property long XPos = {read=FPosition.x, write=FPosition.x, nodefault};
__property long YPos = {read=FPosition.y, write=FPosition.y, nodefault};
public:
__fastcall virtual TSprite(Classes::TComponent* AOwner)
: Classes::TComponent(AOwner) { }
__fastcall virtual ~TSprite(void) { }
};
class TSpriteScene : public TScene
{
typedef TScene inherited;
private:
Classes::TList* FSpriteList;
protected:
__fastcall virtual ~TSpriteScene(void);
virtual void __fastcall SetupSurfaces(System::TObject* Sender);
public:
__fastcall virtual TSpriteScene(Classes::TComponent* AOwner);
virtual void __fastcall DestroyObjects(void);
void __fastcall AddSprite(TSprite* Sprite);
virtual void __fastcall DrawScene(void);
virtual long __fastcall RestoreSurfaces(void);
__property Classes::TList* SpriteList = {read=FSpriteList, write=FSpriteList, nodefault};
__published:
__property TransparentColor;
};
Listing 28.8. The header for the SpriteTest1 program.
///////////////////////////////////////
// Main.h
// Project: SpriteTest1
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\Menus.hpp>
#include "Mercury2.h"
class TForm1 : public TForm
{
__published:
THermes *Hermes1;
TSpriteScene *SpriteScene1;
TSprite *Sprite1;
TSprite *Sprite2;
TMainMenu *MainMenu1;
TMenuItem *Run1;
void __fastcall SpriteScene1SetupSurfaces(TObject *Sender);
void __fastcall Run1Click(TObject *Sender);
void __fastcall FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift);
private:
public:
__fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 28.9. The main form for the TestSprite1 program.
///////////////////////////////////////
// Main.cpp
// Project: SpriteTest1
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma link "Mercury2"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::SpriteScene1SetupSurfaces(TObject *Sender)
{
SpriteScene1->SpriteList->Add(Sprite1);
SpriteScene1->SpriteList->Add(Sprite2);
}
void __fastcall TForm1::Run1Click(TObject *Sender)
{
Hermes1->Run();
}
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key,
TShiftState Shift)
{
if ((Shift.Contains(ssAlt)) && (Key == `X'))
Close();
}
Don't forget that when using this program, you will probably have to explicitly
add Creatures1.pas to your project:
//--------------------------------------------------------------------------
#include <vcl\vcl.h>
#pragma hdrstop
//--------------------------------------------------------------------------
USEFORM("Main.cpp", Form1);
USERES("SpriteTest1.res");
USEUNIT("..\..\Units\creatures1.pas");
//--------------------------------------------------------------------------
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TForm1), &Form1);
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}
Figure 28.3 shows the TestSpriteScene program in action. In this program, I make
no attempt to animate the sprites. To do so, you can merely change the x- and y-coordinates
at which they are shown and then wait for the screen to be updated automatically
by the timer.
Figure 28.3. The TestSpriteScene
program shows a background bitmap with two sprites placed on it.
To work with the TSpriteScene object, you should drop a THermes,
a TSpriteScene, and one or more TSprite objects on a form. Connect
the THermes object to the sprite scene. Pick a background bitmap and a transparent
color for the sprite scene. In the sample program, I use BackGrd2.bmp for
the background and 254 for the transparent color.
For the bitmaps that ship in the Chap28/Media directory, I assume that
the background color is the 254 element in the palette of the background
bitmap. This color is a robin's egg blue, which means that this particular shade
of blue cannot be used in any of the bitmaps except for areas that you want to be
transparent. Don't forget that if a particular RGB pattern such as 255, 0, 255
is designated as the transparent color, you can change any one value to get a nearby
color that is not going to appear as transparent. For example, you can choose 255,
1, 255, which is virtually identical in shade to 255, 0, 255, but it
will not be transparent to the user and can be shown in your bitmaps.
Connect the TSprite objects to a bitmap. I connect the first sprite to
Queen2.bmp and the second sprite to Monk2.bmp.
-
NOTE: These graphical figures were created by
Kari Marcussen and, like all the artwork in this book, are copyrighted and cannot
be used in your own programs. You can, of course, use the art in the "privacy
of your own home," but you cannot distribute programs of any kind, even free
programs, that contain this art.
The XPos and YPos positions for the first sprite are 100x200,
whereas the second sprite is 400x200. You don't have to be concerned with the size
of the sprite itself, as the components will calculate the size automatically at
runtime.
You need to have some way of telling the TSpriteScene object about the
sprites that it owns. I use a simple event handler of TSpriteScene called
OnSetupSurface for this purpose:
void __fastcall TForm1::SpriteScene1SetupSurfaces(TObject *Sender)
{
SpriteScene1->SpriteList->Add(Sprite1);
SpriteScene1->SpriteList->Add(Sprite2);
}
SpriteList is a simple TList descendant. Keeping all the child
sprites in a list allows for easy deallocation of memory:
void __fastcall TSpriteScene::DestroyObjects(void)
{
if(FSpriteList != NULL)
{
for(int i = 0; i < FSpriteList->Count; i++)
((TSprite*)(FSpriteList->Items[i]))->Surface->Release();
FSpriteList->Clear();
}
}
This kind of routine is important because you might want to switch between numerous
TScene descendants during the course of a program. In other words, your
game may have more than one scene in it. I will explain this process in more depth
later in the chapter. However, the quick overview is that you can simply write code
that looks like this:
Hermes1->Scene = Scene1;
... // Code omitted here.
Hermes1->Scene = Scene2;
In this case, the program starts by showing Scene1 and then at some later
point switches to Scene2. When you switch scenes, the DestroyObjects
method for the previous scene is called, thereby deallocating all the memory associated
with background and sprite bitmaps. At the same time, new memory is allocated for
the bitmaps used in Scene2.
The SpriteList also is important when you switch away from a program
in Exclusive mode and then press Alt+Tab to move back to it. At those times, the
following routine is called:
long __fastcall TSpriteScene::RestoreSurfaces(void)
{
TSprite *Sprite;
HRESULT Result;
Result = TScene::RestoreSurfaces();
if(Result == DD_OK)
{
for(int i = 0; i < FSpriteList->Count; i++)
{
Sprite = (TSprite *)FSpriteList->Items[i];
Result = Sprite->Surface->Restore();
if(Result == DD_OK)
DDReloadBitmapLib(FLib, Sprite->Surface, Sprite->Bitmap);
else
break; // Exit on error
}
}
This routine iterates through all the bitmaps in the SpriteList and restores
each surface. This process occurs so quickly that the user simply sees the scene
being restored all at once and is not aware that a list of objects is being re-created.
You can now run the program, trying it first in windowed mode, where the output
will appear a bit muddy because the transparent color probably won't work correctly.
After you're sure everything is running correctly, you can try the program in Exclusive
mode. When in Exclusive mode, you can press Alt+Tab to move away from the main game
window and view some other program such as the Windows Explorer. You can then press
Alt+Tab to go back to the game window. This capability is one of the key features
of DirectX.
If you're concerned about the TransparentColor not working right in windowed
mode, you can try using a common color such as black for your TransparentColor.
It might work correctly, or you can play with the Windows palette system to make
sure the right palette is selected when your program appears in a window. For now,
however, my graphics engine assumes that you want to run your program in Exclusive
mode, and support for windowed mode is available only so that you can easily debug
your programs. After all, DirectX doesn't really provide many advantages over GDI
in windowed mode. The whole point of this process is to switch into Exclusive mode
where you can get the following:
- High performance
- Control over the size of the palette
- Control over the dimensions of the screen
When I say that DirectX offers high performance, I mean that in Exclusive mode
you can write directly to the video buffer, thereby obtaining the same kind of performance
you would expect from a DOS program. The actual degree of performance improvement
you get is dependent on the amount of RAM on your video card and on the bus between
RAM and your video card. The ideal situation is to have a video card with enough
RAM in it to hold all the bitmaps used by any one scene. Therefore, 2MB of memory
on your video card is pretty much a minimum, and a strong argument can be made in
favor of having 4MB. The code that ships with this book will work fine, however,
with a small amount of RAM such as 524KB.
The actual system for drawing to the video buffer involves a process called flipping.
When using this technology, the program draws the image you want to show the user
to an offscreen buffer and then flips that offscreen buffer into the part of video
memory the user sees. The user therefore never sees the relatively lengthy process
of drawing to the buffer but sees only the finished picture when it is blitted, or
"flipped," onto the screen. The core job of the graphics engine presented
in this chapter is to automate this process so you don't have to think about it.
THermesChart: Working with a Tiled World
Some of the most interesting games consist of large worlds that fill up screen
after screen with information. Classic examples of this kind of game are Heroes of
Might and Magic, WarCraft, and Civilization. The THermesChart component
shows how to work with one of these worlds.
Tiled worlds are made up of bitmaps that consist of lots of tiny tiles that can
be combined in various ways to create maps. A picture of one of the tiled bitmaps
is shown in Figure 28.4, and a small portion of the world created from these tiles
is shown in Figure 28.5.
Figure 28.4. The tiles from which
a tiled world is made.
Figure 28.5. A tiled world created
by the THermesChart component from the bitmaps shown in Figure 28.4.
The declaration for the THermesChart component is shown in Listing 28.10,
although the test program for it is shown in Listings 28.11 and 28.12.
Listing 28.10. The declaration for the THermesChart class
and its parent.
struct TSpecialRect
{
bool IsCreature;
RECT R1;
RECT R2;
};
class THermesTiler : public TScene
{
typedef TScene inherited;
friend THermesChart;
private:
int FMaxMapRows;
int FMaxMapCols;
int FBitmapWidth;
int FBitmapHeight;
System::AnsiString FTileMap;
IDirectDrawSurface* FTileSurface;
protected:
__fastcall virtual ~THermesTiler(void);
virtual void __fastcall DrawScene(void);
virtual void __fastcall SetupSurfaces(System::TObject* Sender);
virtual TSpecialRect __fastcall GetRect(int Col, int Row) = 0;
virtual bool __fastcall MoveGrid(int Col, int Row, bool CallFlip) = 0;
public:
__fastcall virtual THermesTiler(Classes::TComponent* AOwner);
virtual void __fastcall DestroyObjects(void);
Windows::TRect __fastcall MapTypeToTileRect(int MapType);
virtual long __fastcall RestoreSurfaces(void);
__published:
__property System::AnsiString TileMap =
{read=FTileMap, write=FTileMap, nodefault};
__property int BitmapWidth =
{read=FBitmapWidth, write=FBitmapHeight, nodefault};
__property int BitmapHeight =
{read=FBitmapHeight, write=FBitmapHeight, nodefault};
};
typedef void __fastcall (__closure *TMoveHeroEvent)(System::TObject* Sender,
const tagPOINT &NewPos, int NewType, bool &MoveOk);
class THermesChart : public THermesTiler
{
typedef THermesTiler inherited;
private:
Creatures1::TCreature* FHero;
bool FHeroActive;
TMoveHeroEvent FOnHeroMove;
bool __fastcall CheckHeroPos(Creatures1::TCreatureList* HeroList,
int Col, int Row);
Windows::TRect __fastcall MapTypeToCreature(int Col, int Row);
virtual TSpecialRect __fastcall GetRect(int Col, int Row);
virtual bool __fastcall MoveGrid(int Col, int Row, bool CallFlip);
protected:
void __fastcall MoveHero(int NewCol, int NewRow);
virtual void __fastcall SetupSurfaces(System::TObject* Sender);
public:
__fastcall virtual ~THermesChart(void);
void __fastcall Move(int Value);
__published:
__property bool HeroActive = {read=FHeroActive, write=FHeroActive, nodefault};
__property TMoveHeroEvent OnHeroMove = {read=FOnHeroMove, write=FOnHeroMove};
public:
__fastcall virtual THermesChart(Classes::TComponent* AOwner)
: Mercury2::THermesTiler(AOwner) { }
};
Listing 28.11. The header file for the TilerTest program.
///////////////////////////////////////
// Main.h
// Project: TilerTest1
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include "Creatures1.hpp"
#include <vcl\Menus.hpp>
#include "Mercury2.h"
class TForm1 : public TForm
{
__published:
THermes *Hermes1;
THermesChart *HermesChart1;
TFileCreatureList *FileCreatureList1;
TMainMenu *MainMenu1;
TMenuItem *Run1;
void __fastcall Run1Click(TObject *Sender);
void __fastcall FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift);
private:
public:
__fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 28.12. The main source for the TestTiler program.
///////////////////////////////////////
// Main.cpp
// Project: TilerTest1
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma link "Creatures1"
#pragma link "Mercury2"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::Run1Click(TObject *Sender)
{
Hermes1->Run();
}
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key,
TShiftState Shift)
{
if (Shift.Contains(ssAlt) && (Key == `X'))
Close();
else
HermesChart1->Move(Key);
}
By now, you should be getting used to the fact that these components are very easy
to use. The only custom code you have to write is to define how to exit the program:
if (Shift.Contains(ssAlt) && (Key == `X'))
Close();
Other than this one simple statement, all the other "coding" involves
nothing more than changing a few properties.
This particular example uses bitmaps that are stored in a DLL called BitDll.dll.
To create a DLL of this type, simply build a resource file containing the bitmaps
you want to use and then add the RC file to a DLL project. You can create a DLL by
choosing File | New | DLL from the BCB menu.
Here is the RC code for a sample resource that contains multiple bitmaps:
Back BITMAP "PANEL4.BMP"
TileMap BITMAP "TILEMAP.BMP"
City BITMAP "FLOOR1.BMP"
Dirs BITMAP "COMPDIRS.BMP"
Treasure BITMAP "TREASURE.BMP"
Sage1 BITMAP "SAGE1.BMP"
You can access the various bitmaps in the DLL by name. For example, you should
set the BackgroundBitmap property of the THermesChart to Back
and the TileMap property to TileMap. As long as the DLLName
property is pointing to a valid DLL, you need do nothing else. All TScene
descendants know how to read bitmaps from either a file on disk or from a DLL.
You'll discover several obvious advantages to using a DLL rather than a raw BMP
file:
- You can ship one file with your game rather than a series of files.
- The DLL helps to conceal your bitmaps from prying eyes. Other developers will
know how to get at them, but most users won't be able to find them.
- You can ship a series of DLLs with your projects and then load and unload the
DLLs as needed to access various different bitmaps. If you switch scenes during the
run of a program, the DLL associated with one scene will be unloaded from memory,
and the DLL associated with the new scene will be loaded.
The tiled world that you create depends on two interrelated binary files. One
binary file contains the screen map that you create, and the second contains a list
of creatures that inhabit the map.
The screen map is simply an array 255x252 characters wide. At this time, the map
you make must be this size, though I will provide alternative sizes at a later date.
Check my Web site for updates.
Each element in the bitmap containing the tiles has a number associated with it.
These numbers are described in the following enumerated type:
enum TMapType {mtGrass, mtWater, mtMountain, mtRoad, mtWater2,
mtFootHill, mtNorthShore, mtWestShore, SouthShore, mtEastShore,
mtSWShore, mtSEShore, mtNWShore, mtNEShore,
mtWNWShore, mtWSEShore, mtESEShore, mtENEShore,
mtBlank1, mtBlank2, mtBlank3, mtBlank4, mtAllSnow,
mtSnowyMountain, mtSouthMtn, mtWestMtn, mtNorthMtn, mtEastMtn,
mtSEMtn, mtSWMtn, mtNWMtn, mtNEMtn, mtNWFootHill,
mtNEFootHill, mtSEFootHill, mtSWFootHill,
mtNorthFootHill, mtEastFootHill, mtSouthFootHill,
mtWestFootHill, mtNEDiagShore, mtSEDiagShore,
mtSWDiagShore, mtNWDiagShore, mtSWBendShore, mtSEBendShore,
mtNWBendShore, mtNEBendShore, mtENBendShore, mtWNBendShore,
mtWSBendShore, mtESBendShore, mtCity, mtCreature};
mtGrass is element 0 in this enumerated type. mtWater
is element 1; mtMountain, element 2; and so on. This type
is not used in the TestTiler1 program, but it will come in handy later in the chapter.
Here is a simple two-dimensional array that encodes a small world:
1 1 1 1 1
1 1 0 1 1
1 0 2 0 1
1 1 0 1 1
1 1 1 1 1
This world consists of a small island with a mountain in the center of it. The
entire island is surrounded by water. Of course, this world is tiny and cannot be
used by the THermesChart component. To find a world that can be used by
THermesChart, look in the Media directory on the CD that accompanies
this book. There you will find a textual representation of a world in a file called
Screen.txt and binary translation of that file called Screen.dta.
A simple program called TextToBinary found on the CD will translate the textual
representation of a world to a binary file. The code that does so takes only a few
lines:
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
FMapPointList = new TMapPointList();
}
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
FMapPointList->Free();
}
void __fastcall TForm1::Button1Click(TObject *Sender)
{
int X, Y;
if (OpenDialog1->Execute())
{
FMapPointList->ReadBinary(OpenDialog1->FileName, X, Y);
AnsiString S = ChangeFileExt(OpenDialog1->FileName, ".txt");
FMapPointList->WriteText(S, X, Y);
}
}
void __fastcall TForm1::Button2Click(TObject *Sender)
{
if (OpenDialog1->Execute())
{
FMapPointList->ReadText(OpenDialog1->FileName);
AnsiString S = ChangeFileExt(OpenDialog1->FileName, ".dta");
FMapPointList->WriteBinary(S,
FMapPointList->StartX, FMapPointList->StartY);
}
}
The constructor and destructor for this program create an object of type TMapPointList.
You can find the TMapPointList object in a file called Creatures1.pas
on the CD. To translate a binary file to text, simply call the methods of TMapPointList
called ReadBinary and WriteText. To reverse the process, call ReadText
and WriteBinary.
-
NOTE: I don't really expect you to create
a world by editing a text file. The file is not ready at the time of this writing,
but the CD that ships with this book may contain a program in the Chap28
directory that will allow you to edit a map from inside Windows by using the mouse.
Check my Web site for updates to this program.
In particular, note that there are methods in Creatures1.pas that make it
easy to create this type of program. For instance, note the following methods of
TCreatureList:
function NameFromLocation(ACol, ARow: Integer): string;
function TypeFromLocation(ACol, ARow: Integer): string;
function CreatureFromLocation(ACol: Integer; ARow: Integer): TCreature;
- Methods like this allow you to quickly identify the tile at a particular location.
You can then use the Map array property from the same object to change these
tiles.
Besides the screen file, you also need to maintain a file that tracks the creatures
shown on your tiled world. In the sample programs that ship with this book, this
file is called Creatures.dta.
You can edit Creatures.dta with a simple program called Entity. Most
of this program is written in Pascal, so I won't discuss its code here. However,
it compiles fine in C++Builder.
-
NOTE: I apologize for including so much Pascal
code in this chapter. For various reasons, I need to have these objects working in
both C++Builder and Delphi, and the simplest way to do that is to write the core
objects and utilities in Object Pascal. In this particular case, I have taken a Delphi
program and swapped out the Pascal version of the project source and substituted
a version written in BCB. That way, I could get the program to compile in C++Builder
by merely changing a few lines of code, all of which are generated automatically
by the IDE. In short, I started a new project, removed the main form, and added the
main form from the Pascal version of the Entities program. That was all I had to
do to translate the program from Delphi to C++Builder.
The Entity program tracks the creature and screen file that it edits through the
Registry. To do so, it uses the same technique I discussed in Chapter 13, "Flat-File,
Real-World Databases." In particular, the following code should jog your memory
as to how I retrieve the current creature and screen file from the Registry:
RegFile := TRegIniFile.Create(`Software\Charlie''s Stuff\Entities');
FCreatureFile := RegFile.ReadString(`FileNames', `CreatureFile', `');
FScreenFile := RegFile.ReadString(`FileNames', `ScreenFile', `');
Running the program once places the proper entries in the Registry. You can then
edit the Registry to enter in the names of the actual files you want to use. In particular,
you should use the Windows utility called RegEdit.exe to edit the area under
HKEY_CURRENT_USER\Charlie's Stuff\Entities.
The creatures file contains a number of creature profiles, as shown in Figure
28.6 and Figure 28.7.
Figure 28.6. The hero's profile
as shown in the Entity program.
Figure 28.7. A hostile creature's
profile as shown in the Entity program.
When you first open the screen, be sure that the hero is located at column 11
and row 11. To set this up properly, you need to edit the Col and
Row fields, as well as the Scrn Col, Scrn Row, Map
Col, and Map Row. You need all these values because you can never see
the whole map at one time. Instead, you see a window into the entire grid. You therefore
need to track where the window is on the grid, where the character is on the grid,
and where the character is on the window currently opened onto the grid. You have
three pairs of values, all of which are tracked for you automatically by the THermesChart
object.
Now that you've examined the screen and creature files, you're ready to run the
TestTiler program. Go ahead and run it once in windowed mode to make sure
that you're reaching all the files in question. Then run the program again in Exclusive
mode.
-
NOTE: You can possibly mangle the screen file
by exiting the TilerTest1 program incorrectly. In other words, if TilerTest1 crashes
in mid-run and you reboot, you can lose the contents of the screen file. To check
whether you've lost the contents, check that your screen file is equal to 64,276
bytes. If it is some other value, the file is probably corrupt and should be refreshed
from the file on the CD that accompanies the book. If you have this problem, you
will probably get a Range error when trying to run the program.
The only point left for you to learn is how to move the character on the screen
and how to scroll around through the world. To do so, simply modify the OnKeyDown
event for TilerTest1:
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key,
TShiftState Shift)
{
if (Shift.Contains(ssAlt) && (Key == `X'))
Close();
else
HermesChart1->Move(Key);
}
This code will exit the program if the user presses Alt+X; otherwise, it will
pass the current keystroke on the HermesChart object. This object responds
to presses on the arrow keys by moving either the main character or by scrolling
the entire map. Press the Insert key to switch between these two modes. Figure 28.8
shows the main character walking on the map.
Figure 28.8.The main character exploring
the shore of a lake, with mountains nearby.
The Mercury2 Source Code
The source for Mercury2.cpp is shown in Listings 28.13 and 28.14. Please
note that several stand-alone functions right at the top of Mercury2.cpp
are only slightly modified versions of code that appears in the DDUtils.cpp
file that ships with the DirectDraw SDK. I could not let you use the original functions,
available on the Microsoft Web site, because they do not take into account the possibility
that the user might want to store bitmaps in a DLL.
Listing 28.13. The header for the Mercury2 unit.
#ifndef Mercury2H
#define Mercury2H
#include <Creatures1.hpp>
#include <ddraw.h>
#include <DsgnIntf.hpp>
#include <OLE2.hpp>
#include <Graphics.hpp>
#include <Forms.hpp>
#include <Controls.hpp>
#include <Classes.hpp>
#include <Windows.hpp>
#include <System.hpp>
namespace Mercury2
{
//-- type declarations ------------------------------------------------------
typedef tagPALETTEENTRY T256PalEntry[256];
typedef tagRGBQUAD TRGB[256];
typedef TRGB *PRGB;
class THermes;
class TScene;
class THermesTiler;
class THermesChart;
class TSpriteScene;
class TDraw;
class TDraw : public Classes::TComponent
{
typedef Classes::TComponent inherited;
friend THermesTiler;
friend THermesChart;
friend TScene;
friend TSpriteScene;
private:
AnsiString FDLLName;
THermes* FHermes;
HANDLE FLib;
IDirectDrawPalette* FPalette;
int FTransparentColor;
bool __fastcall CreateDDSurface(IDirectDrawSurface* &DDS,
System::AnsiString BitmapName, bool UsePalette);
IDirectDrawSurface* __fastcall CreateSurface(HANDLE Bitmap);
HANDLE __fastcall GetDib(HANDLE Instance, System::AnsiString S);
IDirectDrawPalette* __fastcall LoadPalette(HANDLE Instance,
const System::AnsiString BitmapName);
public:
__fastcall virtual TDraw(Classes::TComponent* AOwner);
__fastcall virtual ~TDraw(void);
void __fastcall WriteXY(int X, int Y, System::AnsiString S);
__property int TransparentColor =
{read=FTransparentColor, write=FTransparentColor, nodefault};
__published:
__property AnsiString DLLName =
{read=FDLLName, write=FDLLName};
};
class TScene : public TDraw
{
typedef TDraw inherited;
friend THermes;
private:
System::AnsiString FBackgroundBitmap;
Graphics::TColor FBackColor;
RECT FBackRect;
tagPOINT FBackOrigin;
bool FBlankScene;
bool FShowBitmap;
IDirectDrawSurface* FWorkSurface;
Classes::TNotifyEvent FOnSetupSurfaces;
Classes::TNotifyEvent FOnDrawScene;
public:
__fastcall virtual TScene(Classes::TComponent* AOwner);
virtual void __fastcall DestroyObjects(void);
virtual void __fastcall DrawScene(void);
virtual long __fastcall RestoreSurfaces(void);
virtual void __fastcall SetupSurfaces(System::TObject* Sender);
__published:
__property System::AnsiString BackgroundBitmap =
{read=FBackgroundBitmap, write=FBackgroundBitmap, nodefault};
__property bool BlankScene = {read=FBlankScene, write=FBlankScene, nodefault};
__property long OriginX ={read=FBackOrigin.x, write=FBackOrigin.x, nodefault};
__property long OriginY ={read=FBackOrigin.y, write=FBackOrigin.y, nodefault};
__property bool ShowBitmap = {read=FShowBitmap, write=FShowBitmap, default=1};
__property Classes::TNotifyEvent OnDrawScene =
{read=FOnDrawScene, write=FOnDrawScene};
__property Classes::TNotifyEvent OnSetupSurfaces =
{read=FOnSetupSurfaces, write=FOnSetupSurfaces};
__property Graphics::TColor BackColor =
{read=FBackColor, write=FBackColor, nodefault};
__property TransparentColor ;
public:
__fastcall virtual ~TScene(void) { }
};
class THermes : public Classes::TComponent
{
typedef Classes::TComponent inherited;
friend THermesChart;
friend TDraw;
private:
bool FActive;
Creatures1::TFileCreatureList* FCreatureList;
HWND FHandle;
bool FTimerOdd;
int FTimerInterval;
bool FExclusive;
Classes::TNotifyEvent FPaintProc;
TScene* FScene;
bool FUseTimer;
bool FFirstTime;
IDirectDraw* FDirectDraw;
IDirectDrawSurface* FBackSurface;
IDirectDrawClipper* FClipper;
IDirectDrawSurface* FPrimarySurface;
bool __fastcall CreatePrimary(void);
void __fastcall DDTest(long hr, System::AnsiString S);
void __fastcall InitBaseObjects(void);
bool __fastcall MakeItSo(long DDResult);
void __fastcall SetScene(TScene* Scene);
bool __fastcall SetUpBack(void);
protected:
void __fastcall DrawBitmaps(void);
virtual long __fastcall RestoreSurfaces(void);
public:
__fastcall virtual THermes(Classes::TComponent* AOwner);
__fastcall virtual ~THermes(void) {}
void __fastcall EndExclusive(void);
void __fastcall ErrorEvent( System::AnsiString S);
void __fastcall Flip(void);
virtual void __stdcall InitObjects(void);
virtual void __fastcall Run(void);
__property bool Active = {read=FActive, write=FActive, nodefault};
__property IDirectDrawSurface* BackSurface =
{read=FBackSurface, write=FBackSurface, nodefault};
__property Classes::TNotifyEvent OnDrawBitmap =
{read=FPaintProc, write=FPaintProc};
__published:
__property Creatures1::TFileCreatureList* CreatureList =
{read=FCreatureList, write=FCreatureList, nodefault};
__property bool Exclusive = {read=FExclusive, write=FExclusive, nodefault};
__property TScene* Scene = {read=FScene, write=SetScene, nodefault};
__property int TimerInterval =
{read=FTimerInterval, write=FTimerInterval, nodefault};
__property bool UseTimer = {read=FUseTimer, write=FUseTimer, nodefault};
};
struct TSpecialRect
{
bool IsCreature;
RECT R1;
RECT R2;
};
class THermesTiler : public TScene
{
typedef TScene inherited;
friend THermesChart;
private:
int FMaxMapRows;
int FMaxMapCols;
int FBitmapWidth;
int FBitmapHeight;
System::AnsiString FTileMap;
IDirectDrawSurface* FTileSurface;
protected:
__fastcall virtual ~THermesTiler(void);
virtual void __fastcall DrawScene(void);
virtual void __fastcall SetupSurfaces(System::TObject* Sender);
virtual TSpecialRect __fastcall GetRect(int Col, int Row) = 0;
virtual bool __fastcall MoveGrid(int Col, int Row, bool CallFlip) = 0;
public:
__fastcall virtual THermesTiler(Classes::TComponent* AOwner);
virtual void __fastcall DestroyObjects(void);
Windows::TRect __fastcall MapTypeToTileRect(int MapType);
virtual long __fastcall RestoreSurfaces(void);
__published:
__property System::AnsiString TileMap =
{read=FTileMap, write=FTileMap, nodefault};
__property int BitmapWidth =
{read=FBitmapWidth, write=FBitmapHeight, nodefault};
__property int BitmapHeight =
{read=FBitmapHeight, write=FBitmapHeight, nodefault};
};
typedef void __fastcall (__closure *TMoveHeroEvent)(System::TObject* Sender,
const tagPOINT &NewPos, int NewType, bool &MoveOk);
class THermesChart : public THermesTiler
{
typedef THermesTiler inherited;
private:
Creatures1::TCreature* FHero;
bool FHeroActive;
TMoveHeroEvent FOnHeroMove;
bool __fastcall CheckHeroPos(Creatures1::TCreatureList* HeroList,
int Col, int Row);
Windows::TRect __fastcall MapTypeToCreature(int Col, int Row);
virtual TSpecialRect __fastcall GetRect(int Col, int Row);
virtual bool __fastcall MoveGrid(int Col, int Row, bool CallFlip);
protected:
void __fastcall MoveHero(int NewCol, int NewRow);
virtual void __fastcall SetupSurfaces(System::TObject* Sender);
public:
__fastcall virtual ~THermesChart(void);
void __fastcall Move(int Value);
__published:
__property bool HeroActive = {read=FHeroActive, write=FHeroActive, nodefault};
__property TMoveHeroEvent OnHeroMove = {read=FOnHeroMove, write=FOnHeroMove};
public:
__fastcall virtual THermesChart(Classes::TComponent* AOwner)
: Mercury2::THermesTiler(AOwner) { }
};
class TSprite;
class TSprite : public Classes::TComponent
{
typedef Classes::TComponent inherited;
friend TSpriteScene;
private:
System::AnsiString FBitmap;
tagPOINT FPosition;
IDirectDrawSurface* FSurface;
RECT FRect;
public:
bool __fastcall IsHit(int X, int Y);
__property IDirectDrawSurface* Surface=
{read=FSurface, write=FSurface, nodefault};
__property RECT Rect = {read=FRect, write=FRect};
__published:
__property System::AnsiString Bitmap={read=FBitmap, write=FBitmap, nodefault};
__property long XPos = {read=FPosition.x, write=FPosition.x, nodefault};
__property long YPos = {read=FPosition.y, write=FPosition.y, nodefault};
public:
__fastcall virtual TSprite(Classes::TComponent* AOwner)
: Classes::TComponent(AOwner) { }
__fastcall virtual ~TSprite(void) { }
};
class TSpriteScene : public TScene
{
typedef TScene inherited;
private:
Classes::TList* FSpriteList;
protected:
__fastcall virtual ~TSpriteScene(void);
virtual void __fastcall SetupSurfaces(System::TObject* Sender);
public:
__fastcall virtual TSpriteScene(Classes::TComponent* AOwner);
virtual void __fastcall DestroyObjects(void);
void __fastcall AddSprite(TSprite* Sprite);
virtual void __fastcall DrawScene(void);
virtual long __fastcall RestoreSurfaces(void);
__property Classes::TList* SpriteList = {read=FSpriteList, write=FSpriteList,nodefault};
__published:
__property TransparentColor;
};
////////////////////////////////////////
// TSceneEditor ////////////////////////
////////////////////////////////////////
class TSceneEditor: public TComponentEditor
{
protected:
virtual __fastcall void Edit(void);
public:
virtual __fastcall TSceneEditor(TComponent *AOwner, TFormDesigner *Designer)
: TComponentEditor(AOwner, Designer) {}
};
//-- var, const, procedure --------------------------------------------------
#define Timer1 (Byte)(1)
extern void __fastcall Register(void);
}/* namespace Mercury1 */
#if !defined(NO_IMPLICIT_NAMESPACE_USE)
using namespace Mercury2;
#endif
//-- end unit ----------------------------------------------------------------
#endif // Mercury2
Listing 28.14. The main source file for the Mercury2 module.
///////////////////////////////////////
// Mercury2.cpp
// DirectX Graphics
// Copyright (c) 1997 by Charlie Calvert
// Thanks to John Thomas, Stuart Fullmar and Jeff Cottingham
///////////////////////////////////////
#include <vcl\vcl.h>
#pragma hdrstop
#include "Mercury2.h"
#include "errors1.h"
#include "SceneEditor1.h"
#pragma link "Errors1.obj"
#pragma link "SceneEditor1.obj"
/*{ ------------------------ }
{ --- THermes ------------ }
{ ------------------------ } */
THermes *AHermes;
void Timer2Timer(HWND H, UINT Msg, UINT Event, DWORD Time)
{
if (AHermes->Active)
AHermes->Flip();
}
namespace Mercury2
{
// Slightly modified version of code from DDUtils.cpp
HRESULT DDCopyBitmap(IDirectDrawSurface *pdds,
HBITMAP hbm, int x, int y, int dx, int dy)
{
HDC hdcImage;
HDC hdc;
BITMAP bm;
DDSURFACEDESC ddsd;
HRESULT hr;
if (hbm == NULL || pdds == NULL)
return E_FAIL;
//
// make sure this surface is restored.
//
pdds->Restore();
//
// select bitmap into a memoryDC so we can use it.
//
hdcImage = CreateCompatibleDC(NULL);
if (!hdcImage)
OutputDebugString("createcompatible dc failed\n");
SelectObject(hdcImage, hbm);
//
// get size of the bitmap
//
GetObject(hbm, sizeof(bm), &bm); // get size of bitmap
dx = dx == 0 ? bm.bmWidth : dx; // use the passed size, unless zero
dy = dy == 0 ? bm.bmHeight : dy;
//
// get size of surface.
//
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_HEIGHT | DDSD_WIDTH;
pdds->GetSurfaceDesc(&ddsd);
if ((hr = pdds->GetDC(&hdc)) == DD_OK)
{
StretchBlt(hdc, 0, 0, ddsd.dwWidth, ddsd.dwHeight, hdcImage, x, y, dx, dy, SRCCOPY);
pdds->ReleaseDC(hdc);
}
DeleteDC(hdcImage);
return hr;
}
// Slightly modified version of code from DDUtils.cpp
void DDReloadBitmapLib(HANDLE Lib, IDirectDrawSurface *Surface,
const AnsiString BitmapName)
{
HBITMAP Bitmap;
if (Lib)
Bitmap = LoadImage(Lib, BitmapName.c_str(),
IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);
else
Bitmap = LoadImage(GetModuleHandle(NULL), BitmapName.c_str(),
IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);
if (!Bitmap)
Bitmap = LoadImage(0, BitmapName.c_str(), IMAGE_BITMAP,
0, 0, LR_LOADFROMFILE | LR_CREATEDIBSECTION);
if (!Bitmap)
throw Exception("Unable to load bitmap %s",
OPENARRAY(TVarRec, (BitmapName)));
DDCopyBitmap(Surface, Bitmap, 0, 0, 0, 0);
DeleteObject(Bitmap);
}
// Slightly modified version of code from DDUtils.cpp
IDirectDrawSurface *DDCreateSurface(IDirectDraw *DD, DWORD Width, DWORD Height,
bool SysMem, bool Trans, DWORD dwColorKey)
{
DDSURFACEDESC SurfaceDesc;
HRESULT hr;
DDCOLORKEY ColorKey;
IDirectDrawSurface *Surface;
// fill in surface desc
memset(&SurfaceDesc, 0, sizeof(DDSURFACEDESC));
SurfaceDesc.dwSize = sizeof(SurfaceDesc);
SurfaceDesc.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH;
SurfaceDesc.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
SurfaceDesc.dwHeight = Height;
SurfaceDesc.dwWidth = Width;
hr = DD->CreateSurface(&SurfaceDesc, &Surface, NULL);
// set the color key for this bitmap
if (hr == DD_OK)
{
if (Trans)
{
ColorKey.dwColorSpaceLowValue = dwColorKey;
ColorKey.dwColorSpaceHighValue = dwColorKey;
Surface->SetColorKey(DDCKEY_SRCBLT, &ColorKey);
}
}
else
throw EDDError("CreateSurface Failed in DDCreateSurface");
return Surface;
} // DDCreateSurface
IDirectDrawClipper *CreateClipper(IDirectDraw *DD, HWND Handle)
{
HRESULT hr;
IDirectDrawClipper *lpClipper;
hr = DD->CreateClipper(0, &lpClipper, NULL);
if (hr != DD_OK)
{
throw EDDError("No Clipper");
}
hr = lpClipper->SetHWnd(0, Handle);
if (hr != DD_OK)
{
throw EDDError("Can''t set clipper window handle");
}
return lpClipper;
}
}
__fastcall THermes::THermes(TComponent* AOwner)
:TComponent(AOwner)
{
FHandle = ((TWinControl *)(AOwner))->Handle;
FFirstTime = true;
}// THermes
void __fastcall THermes::EndExclusive()
{
FDirectDraw->FlipToGDISurface();
if (FExclusive)
FDirectDraw->SetCooperativeLevel(FHandle, DDSCL_NORMAL);
}// EndExclusive
void __fastcall THermes::ErrorEvent(String S)
{
FActive = false;
EndExclusive();
throw EDDError(S);
}// ErrorEvent
void __fastcall THermes::Run()
{
InitObjects();
Flip();
}// Run
void __fastcall THermes::DDTest(long hr, System::AnsiString S)
{
if (!Windows::Succeeded(hr))
throw EDDError("DDTest Error: %s $%x %s",
OPENARRAY(TVarRec, (S, int(hr), AnsiString(GetOleError(hr)))));
}// DDTest
/////////////////////////////////////////////////
// Create the primary surface
/////////////////////////////////////////////////
bool __fastcall THermes::CreatePrimary(void)
{
DDSURFACEDESC SurfaceDesc;
HResult hr;
bool Result = true;
memset(&SurfaceDesc, 0, sizeof(SurfaceDesc));
SurfaceDesc.dwSize = sizeof(SurfaceDesc);
if (!FExclusive){
SurfaceDesc.dwFlags = DDSD_CAPS;
SurfaceDesc.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;
}
else{
SurfaceDesc.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
SurfaceDesc.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE |
DDSCAPS_FLIP |
DDSCAPS_COMPLEX;
SurfaceDesc.dwBackBufferCount = 1;
};
hr = FDirectDraw->CreateSurface(&SurfaceDesc, &FPrimarySurface, NULL);
if (hr != DD_OK)
throw EDDError("THermes.CreatePrimary: %d %s",
OPENARRAY(TVarRec, (int(hr), GetOleError(hr))));
else
return Result;
}// CreatePrimary
void __fastcall THermes::SetScene(TScene *Scene)
{
if (Scene)
Scene->DestroyObjects();
FScene = Scene;
}// SetScene
bool __fastcall THermes::SetUpBack(void)
{
bool Result = false;
HResult hr;
DDSCAPS DDSCaps;
if (!FExclusive)
{
FBackSurface =
Mercury2::DDCreateSurface(FDirectDraw, 640, 480, false, false, 0);
if (FBackSurface == NULL)
throw EDDError("Can''t set up back surface");
FClipper = Mercury2::CreateClipper(FDirectDraw, FHandle);
hr = FPrimarySurface->SetClipper(FClipper);
if( hr != DD_OK )
throw EDDError("Can''t attach clipper to front buffer");
}
else
{
memset(&DDSCaps, 0, sizeof(DDSCaps));
DDSCaps.dwCaps = DDSCAPS_BACKBUFFER;
hr = FPrimarySurface->GetAttachedSurface(&DDSCaps, &FBackSurface);
if (hr != DD_OK)
throw EDDError("TSpeedDraw.SetUpBack: %d %s",
OPENARRAY(TVarRec, (int(hr), GetOleError(hr))));
else
Result = true;
};
return Result;
}// SetUpBack
void __fastcall THermes::InitBaseObjects()
{
DWORD Flags;
HResult hr;
DDTest(DirectDrawCreate(NULL, &FDirectDraw, NULL), "InitObjects1");
if (!FExclusive)
Flags = DDSCL_NORMAL;
else
Flags = DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN;
DDTest(FDirectDraw->SetCooperativeLevel(FHandle, Flags),"SetCooperativeLevel");
if (FExclusive)
{
hr = FDirectDraw->SetDisplayMode(640, 480, 8);
if(hr != DD_OK)
throw EDDError("TSpeedDraw.InitObjects: %d %s",
OPENARRAY(TVarRec, (int(hr), GetOleError(hr))));
};
CreatePrimary();
SetUpBack();
if (FCreatureList != NULL) FCreatureList->ReadFiles();
}// InitBaseObjects
/* Here are the steps in the initialization:
Create DirectDraw Object
SetCooperativeLevel
if Exclusive then SetDisplayMode
CreatePrimary
SetupBack
Create the work surface
Set Active to true */
void __stdcall THermes::InitObjects(void)
{
AHermes = this;
if (FFirstTime)
{
InitBaseObjects();
FFirstTime = false;
};
if ((Scene) && (Scene->FBackgroundBitmap != ""))
Scene->SetupSurfaces(this);
if (FUseTimer)
SetTimer(FHandle, Timer1, FTimerInterval, (FARPROC)Timer2Timer);
FActive = true;
}// InitObjects
void __fastcall THermes::DrawBitmaps()
{
if (Scene) Scene->DrawScene();
}
bool __fastcall THermes::MakeItSo(HResult DDResult)
{
bool Result;
switch(DDResult)
{
case DD_OK:
Result = true;
break;
case DDERR_SURFACELOST:
Result = (RestoreSurfaces() == DD_OK);
break;
default:
Result = DDResult != DDERR_WASSTILLDRAWING;
break;
};
return Result;
} // MakeItSo
long __fastcall THermes::RestoreSurfaces()
{
HRESULT Result;
Result = FPrimarySurface->Restore();
if ((Result == DD_OK) && (Scene))
Result = Scene->RestoreSurfaces();
return Result;
}// RestoreSurfaces
void __fastcall THermes::Flip(void)
{
RECT R1, R;
FTimerOdd = !FTimerOdd;
if (!FActive)
return;
if (!FExclusive)
{
try
{
DrawBitmaps();
GetWindowRect(FHandle, &R);
R1 = Rect(0, 0, 640, 480);
DDTest(FPrimarySurface->Blt(&R, FBackSurface, &R1, 0, NULL), "Flip");
}
catch(Exception &E)
{
ErrorEvent("Flipping");
};
}
else
{
try
{
if (FActive)
{
do
{
Application->ProcessMessages();
} while(!MakeItSo(FPrimarySurface->Flip(NULL, DDFLIP_WAIT)));
DrawBitmaps();
}
}
catch(...)
{
ErrorEvent("Flipping");
}
}
}// Flip
///////////////////////////////////////
// TDraw //////////////////////////////
///////////////////////////////////////
__fastcall TDraw::TDraw(Classes::TComponent* AOwner)
: TComponent(AOwner)
{
}
__fastcall TDraw::~TDraw(void)
{
if (FLib != NULL)
FreeLibrary(FLib);
}
bool __fastcall TDraw::CreateDDSurface(IDirectDrawSurface* &DDS,
System::AnsiString BitmapName, bool UsePalette)
{
bool Result;
DDCOLORKEY ColorKey;
HRESULT hr;
HANDLE Dib;
if (UsePalette)
{
FPalette = LoadPalette(FLib, BitmapName.c_str());
if (!FPalette)
throw EDDError("LoadPalette Failed");
FHermes->FPrimarySurface->SetPalette(FPalette);
}
Dib = GetDib(FLib, BitmapName);
DDS = CreateSurface(Dib);
if (!DDS)
throw EDDError("CreateSurface Failed");
ColorKey.dwColorSpaceLowValue = FTransparentColor;
ColorKey.dwColorSpaceHighValue = FTransparentColor;
hr = DDS->SetColorKey(DDCKEY_SRCBLT, &ColorKey);
if (hr != DD_OK)
throw EDDError("TSpeedDraw.CreateDDSurface: %d %s",
OPENARRAY(TVarRec, (int(hr), GetOleError(hr))));
else
Result = True;
return Result;
}
IDirectDrawSurface* __fastcall TDraw::CreateSurface(HANDLE Bitmap)
{
DDSURFACEDESC SurfaceDesc;
Windows::TBitmap BM;
IDirectDrawSurface *Result;
if (Bitmap == 0)
throw Exception("No Bitmap in CreateSurface");
try
{
GetObject(Bitmap, sizeof(BM), &BM);
memset(&SurfaceDesc, 0, sizeof(SurfaceDesc));
SurfaceDesc.dwSize = sizeof(SurfaceDesc);
SurfaceDesc.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH;
SurfaceDesc.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
SurfaceDesc.dwWidth = BM.bmWidth;
SurfaceDesc.dwHeight = BM.bmHeight;
if (FHermes->FDirectDraw->CreateSurface(&SurfaceDesc, &Result, NULL) != DD_OK)
throw Exception("CreateSurface failed");
DDCopyBitmap(Result, Bitmap, 0, 0, 0, 0);
}
catch(Exception &E)
{
FHermes->ErrorEvent("TSpeedDraw.CreateSurface: " + E.Message);
}
return Result;
}
HANDLE __fastcall TDraw::GetDib(HANDLE Instance, System::AnsiString S)
{
HANDLE Result;
UINT Flags;
if (Instance != 0)
Flags = LR_CREATEDIBSECTION;
else
Flags = LR_LOADFROMFILE | LR_CREATEDIBSECTION;
Result = LoadImage((HANDLE)Instance, S.c_str(), IMAGE_BITMAP,
0, 0, Flags);
if (Result == 0)
FHermes->ErrorEvent("TSpeedDraw.GetDib: Could not load bitmap");
return Result;
}
IDirectDrawPalette* __fastcall TDraw::LoadPalette(HANDLE Instance,
const System::AnsiString BitmapName)
{
IDirectDrawPalette* ddpal;
int i;
int n;
int fh;
HRSRC h;
LPBITMAPINFO BitmapInfo;
PALETTEENTRY ape[256];
RGBQUAD * RGBQuad;
//
// build a 332 palette as the default.
//
for (i=0; i<256; i++)
{
ape[i].peRed = (BYTE)(((i >> 5) & 0x07) * 255 / 7);
ape[i].peGreen = (BYTE)(((i >> 2) & 0x07) * 255 / 7);
ape[i].peBlue = (BYTE)(((i >> 0) & 0x03) * 255 / 3);
ape[i].peFlags = (BYTE)0;
}
if (BitmapName == "")
FHermes->ErrorEvent("No bitmapname in LoadPalette");
//
// get a pointer to the bitmap resource.
//
if (Instance)
{
h = FindResource(Instance, BitmapName.c_str(), RT_BITMAP);
if (h)
{
BitmapInfo = (LPBITMAPINFO)LockResource(LoadResource(Instance, h));
if (!BitmapInfo)
throw EDDError("No LockResouce: " + this->ClassName());
RGBQuad = (RGBQUAD*)BitmapInfo->bmiColors;
if (BitmapInfo == NULL || BitmapInfo->bmiHeader.biSize <sizeof(BITMAPINFOHEADER))
n = 0;
else if (BitmapInfo->bmiHeader.biBitCount > 8)
n = 0;
else if (BitmapInfo->bmiHeader.biClrUsed == 0)
n = 1 << BitmapInfo->bmiHeader.biBitCount;
else
n = BitmapInfo->bmiHeader.biClrUsed;
//
// a DIB color table has its colors stored BGR not RGB
// so flip them around.
//
for(i=0; i<n; i++ )
{
ape[i].peRed = RGBQuad[i].rgbRed;
ape[i].peGreen = RGBQuad[i].rgbGreen;
ape[i].peBlue = RGBQuad[i].rgbBlue;
ape[i].peFlags = 0;
}
}
}
else
{
fh = _lopen(BitmapName.c_str(), OF_READ);
if (fh != -1)
{
BITMAPFILEHEADER bf;
BITMAPINFOHEADER bi;
_lread(fh, &bf, sizeof(bf));
_lread(fh, &bi, sizeof(bi));
_lread(fh, ape, sizeof(ape));
_lclose(fh);
if (bi.biSize != sizeof(BITMAPINFOHEADER))
n = 0;
else if (bi.biBitCount > 8)
n = 0;
else if (bi.biClrUsed == 0)
n = 1 << bi.biBitCount;
else
n = bi.biClrUsed;
//
// a DIB color table has its colors stored BGR not RGB
// so flip them around.
//
for(i=0; i<n; i++ )
{
BYTE r = ape[i].peRed;
ape[i].peRed = ape[i].peBlue;
ape[i].peBlue = r;
}
}
}
FHermes->FDirectDraw->CreatePalette(DDPCAPS_8BIT, ape, &ddpal, NULL);
return ddpal;
}
void __fastcall TDraw::WriteXY(int X, int Y, System::AnsiString S)
{
HDC DC;
FHermes->BackSurface->GetDC(&DC);
SetBkMode(DC, TRANSPARENT);
TextOut(DC, X, Y, S.c_str(), S.Length());
FHermes->BackSurface->ReleaseDC(DC);
}
// ------------------------
// -- TScene --------------
// ------------------------
__fastcall TScene::TScene(TComponent *AOwner) : TDraw(AOwner)
{
ShowBitmap = true;
}
void __fastcall TScene::DestroyObjects(void)
{
if(FWorkSurface != NULL)
FWorkSurface->Release();
FWorkSurface = NULL;
}
long _fastcall TScene::RestoreSurfaces(void)
{
long Result;
Result = FWorkSurface->Restore();
if(Result == DD_OK)
DDReloadBitmapLib(FLib, FWorkSurface, FBackgroundBitmap);
return(Result);
}
void __fastcall TScene::SetupSurfaces(System::TObject *Sender)
{
AnsiString ErrStr = "TSpeedDraw.SetupWorkSurface: No Surface Desc %d %s";
DDSURFACEDESC SurfaceDesc;
HRESULT hr;
FHermes = dynamic_cast <THermes *>(Sender);
if(FDLLName != "")
{
if(!FLib)
FLib = LoadLibrary(FDLLName.c_str());
if(int(FLib) < 32)
throw EDDError("No Library");
}
if(!CreateDDSurface(FWorkSurface, FBackgroundBitmap, True))
throw EDDError("TSpeedDraw.SetupWorkSurface: No WorkSurface: " +
FBackgroundBitmap);
else
{
SurfaceDesc.dwSize = sizeof(SurfaceDesc);
hr = FWorkSurface->GetSurfaceDesc(&SurfaceDesc);
if(hr != DD_OK)
throw EDDError(ErrStr, OPENARRAY(TVarRec, (int(hr), GetOleError(hr))));
FBackRect = Rect(0, 0, SurfaceDesc.dwWidth, SurfaceDesc.dwHeight);
}
if(FOnSetupSurfaces)
FOnSetupSurfaces(this);
}
void _fastcall TScene::DrawScene()
{
AnsiString ErrStr = "TSpeedDraw.BackGroundBlits: $%x \r %s \r %s";
HRESULT hr;
HDC DC;
HBRUSH OldBrush, Brush;
if (FBlankScene)
{
FHermes->BackSurface->GetDC(&DC); // Don't step through!!
Brush = CreateSolidBrush(FBackColor);
OldBrush = SelectObject(DC, Brush);
Rectangle(DC, 0, 0, 640, 480);
SelectObject(DC, OldBrush);
DeleteObject(Brush);
FHermes->BackSurface->ReleaseDC(DC);
}
if (FShowBitmap)
{
hr = AHermes->BackSurface->BltFast(FBackOrigin.x, FBackOrigin.y,
FWorkSurface, &FBackRect,
DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY);
if (!Windows::Succeeded(hr))
throw EDDError(ErrStr, OPENARRAY(TVarRec,
(int(hr), GetOleError(hr), "Check BackRect, BackOrigin???")));
}
if (FOnDrawScene)
FOnDrawScene(this);
}
///////////////////////////////////////
// THermesTiler ///////////////////////
///////////////////////////////////////
__fastcall THermesTiler::THermesTiler(Classes::TComponent* AOwner)
:TScene(AOwner)
{
FBitmapWidth = 32;
FBitmapHeight = 32;
FMaxMapRows = 12;
FMaxMapCols = 20;
}
__fastcall THermesTiler::~THermesTiler(void)
{
DestroyObjects();
}
void __fastcall THermesTiler::DrawScene(void)
{
TScene::DrawScene();
int i, j, k;
TSpecialRect SpR;
if (FTileSurface == NULL)
return;
k = 1;
for (j = 1; j <= FMaxMapRows; j++)
for (i = 1; i <= FMaxMapCols; i++)
{
SpR = GetRect(i + FHermes->CreatureList->MapCol, j + FHermes->CreatureList->MapRow);
FHermes->BackSurface->BltFast(FBitmapWidth * (k - 1),
FBitmapHeight * (j - 1), FTileSurface, &SpR.R1, DDBLTFAST_SRCCOLORKEY);
if (SpR.IsCreature) // There's a character or building here
FHermes->BackSurface->BltFast(FBitmapWidth * (k - 1),
FBitmapHeight * (j - 1), FTileSurface, &SpR.R2, DDBLTFAST_SRCCOLORKEY);
if ((i % 20) == 0)
k = 1;
else
k++;
}
}
void __fastcall THermesTiler::SetupSurfaces(System::TObject* Sender)
{
TScene::SetupSurfaces(Sender);
if (FTileMap != "")
if (!CreateDDSurface(FTileSurface, FTileMap, True))
throw Exception("THermesTiler.InitObjects");
}
void __fastcall THermesTiler::DestroyObjects(void)
{
if (FTileMap != "")
if (FTileSurface != NULL)
FTileSurface->Release();
FTileSurface = NULL;
}
Windows::TRect __fastcall THermesTiler::MapTypeToTileRect(int MapType)
{
int X, Y = 0;
if ((MapType > 19) && (MapType < 41))
{
Y = FBitmapHeight;
X = (MapType - 21) * FBitmapWidth;
}
else if (MapType > 39)
{
Y = 64;
X = (MapType - 41) * FBitmapWidth;
}
else
{
X = MapType * FBitmapWidth;
}
return Rect(X, Y, X + FBitmapWidth, FBitmapHeight + Y);
}
long __fastcall THermesTiler::RestoreSurfaces(void)
{
long Result = TScene::RestoreSurfaces();
if (Result == DD_OK)
{
Result = FTileSurface->Restore();
if (Result == DD_OK)
if (FTileMap != "")
DDReloadBitmapLib(FLib, FTileSurface, FTileMap);
}
return Result;
}
///////////////////////////////////////
// THermesChart ///////////////////////
///////////////////////////////////////
bool __fastcall THermesChart::CheckHeroPos(Creatures1::TCreatureList* HeroList,
int Col, int Row)
{
int NewPosType;
TPoint NewPos;
bool MoveOk = True;
if (OnHeroMove != NULL)
{
NewPos.x = FHero->TrueCol + Col;
NewPos.y = FHero->TrueRow + Row;
NewPosType = HeroList->GetMapType(NewPos.x, NewPos.y);
OnHeroMove(this, NewPos, NewPosType, MoveOk);
}
return MoveOk;
}
Windows::TRect __fastcall THermesChart::MapTypeToCreature(int Col, int Row)
{
TCreature *Creature = FHermes->CreatureList->CreatureFromLocation(Col, Row);
if (Creature->TypeStr == "Hero")
return MapTypeToTileRect(3);
else
return MapTypeToTileRect(StrToInt(Creature->ID));
}
TSpecialRect __fastcall THermesChart::GetRect(int Col, int Row)
{
Byte MapType;
TSpecialRect Result;
MapType = FHermes->CreatureList->Map[Col][Row];
if ((FHermes->FTimerOdd == True) && (MapType == 1 /*mtWater*/))
MapType = 4; // mtWater2;
if (BitOn(7, MapType))
{
Result.IsCreature = True;
SetBit(7, 0, MapType);
Result.R1 = MapTypeToTileRect(MapType);
Result.R2 = MapTypeToCreature(Col, Row);
}
else
{
Result.IsCreature = False;
Result.R1 = MapTypeToTileRect(MapType);
}
return Result;
}
bool __fastcall THermesChart::MoveGrid(int Col, int Row, bool CallFlip)
{
TPoint P;
if (CheckHeroPos(FHermes->CreatureList, Col, Row))
{
P.x = FHermes->CreatureList->MapCol + Col;
P.y = FHermes->CreatureList->MapRow + Row;
if ((P.x < 0) ||
(P.x > (FHermes->CreatureList->MaxCols - FMaxMapCols)) ||
(P.y < 0) ||
(P.y > (FHermes->CreatureList->MaxRows - FMaxMapRows)))
return False;
FHermes->CreatureList->MapCol = P.x;
FHermes->CreatureList->MapRow = P.y;
FHermes->CreatureList->MoveCreature(FHero, Col, Row);
FHermes->Flip();
return True;
}
return False;
}
__fastcall THermesChart::~THermesChart(void)
{
}
void __fastcall THermesChart::MoveHero(int NewCol, int NewRow)
{
TPoint P;
if (!CheckHeroPos(FHermes->CreatureList, NewCol, NewRow))
return;
P.x = FHero->ScreenCol + NewCol;
P.y = FHero->ScreenRow + NewRow;
if ((P.x <= 0) || (P.x > (FMaxMapCols)) ||
(P.y <= 1) || (P.y > FMaxMapRows))
return;
FHero->ScreenCol = P.x;
FHero->ScreenRow = P.y;
FHermes->CreatureList->MoveCreature(FHero, NewCol, NewRow);
FHermes->Flip();
}
void __fastcall THermesChart::SetupSurfaces(System::TObject* Sender)
{
THermesTiler::SetupSurfaces(Sender);
if (FHermes->CreatureList == NULL)
throw EDDError("CreatureList cannot be blank.");
FHeroActive = True;
FHero = FHermes->CreatureList->CreatureFromName("Hero");
}
void __fastcall THermesChart::Move(int Value)
{
if (Value == VK_INSERT)
HeroActive = !HeroActive;
else if (HeroActive)
switch (Value)
{
case VK_RIGHT: MoveHero(1, 0); break;
case VK_LEFT: MoveHero(-1, 0); break;
case VK_DOWN: MoveHero(0, 1); break;
case VK_UP: MoveHero(0, -1); break;
}
else
switch(Value)
{
case VK_RIGHT: MoveGrid(1, 0, False); break;
case VK_LEFT: MoveGrid(-1, 0, False); break;
case VK_DOWN: MoveGrid(0, 1, False); break;
case VK_UP: MoveGrid(0, -1, False); break;
}
}
/* ------------------------ */
/* -- TSprite ------------- */
/* ------------------------ */
bool __fastcall TSprite::IsHit(int X, int Y)
{
X = X - XPos;
Y = Y - YPos;
if ((X >= 0) && (Y >= 0) && (X <= Rect.right) && (Y <= Rect.bottom))
return true;
else
return false;
}
/* ------------------------ */
/* -- TSpriteScene -------- */
/* ------------------------ */
__fastcall TSpriteScene::TSpriteScene(TComponent *AOwner)
: TScene(AOwner)
{
FSpriteList = new TList();
}
__fastcall TSpriteScene::~TSpriteScene()
{
DestroyObjects();
delete FSpriteList;
}
void __fastcall TSpriteScene::DestroyObjects(void)
{
if(FSpriteList != NULL)
{
for(int i = 0; i < FSpriteList->Count; i++)
((TSprite*)(FSpriteList->Items[i]))->Surface->Release();
FSpriteList->Clear();
}
}
void __fastcall TSpriteScene::AddSprite(TSprite *Sprite)
{
SpriteList->Add(Sprite);
}
void __fastcall TSpriteScene::DrawScene(void)
{
const char ErrStr[] = "TSpriteScene.DrawScene";
HResult hr;
TSprite *Sprite;
TScene::DrawScene();
for(int i = 0; i < FSpriteList->Count; i++)
{
Sprite = (TSprite *)FSpriteList->Items[i];
hr = FHermes->BackSurface->BltFast(Sprite->XPos, Sprite->YPos,
Sprite->Surface, &Sprite->FRect, DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY);
if(!Windows::Succeeded(hr))
throw(EDDError(ErrStr, OPENARRAY(TVarRec, (int(hr), GetOleError(hr),
"Check BackRect, BackOrigin???"))));
}
}
long __fastcall TSpriteScene::RestoreSurfaces(void)
{
TSprite *Sprite;
HRESULT Result;
Result = TScene::RestoreSurfaces();
if(Result == DD_OK)
{
for(int i = 0; i < FSpriteList->Count; i++)
{
Sprite = (TSprite *)FSpriteList->Items[i];
Result = Sprite->Surface->Restore();
if(Result == DD_OK)
DDReloadBitmapLib(FLib, Sprite->Surface, Sprite->Bitmap);
else
break; // Exit on error
}
}
return((long)Result);
}
void __fastcall TSpriteScene::SetupSurfaces(TObject *Sender)
{
IDirectDrawSurface *Surface;
TSprite *Sprite;
DDSURFACEDESC SurfaceDesc;
HRESULT hr;
TScene::SetupSurfaces(Sender);
if(SpriteList == NULL)
return;
for(int i = 0; i < SpriteList->Count; i++)
{
Sprite = (TSprite *)SpriteList->Items[i];
if(!CreateDDSurface(Surface, Sprite->Bitmap, False))
throw EDDError("Could not create surface");
SurfaceDesc.dwSize = sizeof(SurfaceDesc);
hr = Surface->GetSurfaceDesc(&SurfaceDesc);
if(hr == DD_OK)
Sprite->Rect = Rect(0, 0, SurfaceDesc.dwWidth, SurfaceDesc.dwHeight);
else
throw EDDError("No SurfaceDesc");
Sprite->Surface = Surface;
}
}
void __fastcall TSceneEditor::Edit(void)
{
RunSceneEditorDlg((TScene *)Component);
}
namespace Mercury2
{
void __fastcall Register()
{
TComponentClass classes[5] = {__classid(THermes), __classid(TScene),
__classid(THermesChart), __classid(TSpriteScene), __classid(TSprite)};
RegisterComponents("Unleash", classes, 4);
RegisterComponentEditor(__classid(TScene), __classid(TSceneEditor));
}
}
Summary
In this chapter, you learned how to create a simple graphics engine that gives
you access to DirectX. In particular, you learned how to do the following:
- Get into and out of Exclusive mode
- Restore surfaces when the user switches away from a DirectX application
- Create backgrounds and sprites
- Work with a tiled world that expands far beyond the reaches of the screen
In the next chapter, I add a simple game engine to this graphics engine and then
give a simple example of how to use these tools to create the elements of a strategy
game.
|